package org.andengine.entity.sprite;

import org.andengine.entity.sprite.vbo.ITiledSpriteVertexBufferObject;
import org.andengine.opengl.shader.PositionColorTextureCoordinatesShaderProgram;
import org.andengine.opengl.shader.ShaderProgram;
import org.andengine.opengl.texture.region.ITiledTextureRegion;
import org.andengine.opengl.vbo.DrawType;
import org.andengine.opengl.vbo.VertexBufferObjectManager;
import org.andengine.util.time.TimeConstants;

/**
 * (c) 2010 Nicolas Gramlich
 * (c) 2011 Zynga Inc.
 *
 * @author Nicolas Gramlich
 * @since 15:25:46 - 10.03.2010
 */
public class AnimatedSprite extends TiledSprite {
    // ===========================================================
    // Constants
    // ===========================================================

    private static final int FRAMEINDEX_INVALID = -1;

    // ===========================================================
    // Fields
    // ===========================================================

    private boolean mAnimationRunning;
    private boolean mAnimationStartedFired;

    private int mCurrentFrameIndex;
    private long mAnimationProgress;
    private int mRemainingLoopCount;

    @SuppressWarnings("deprecation")
    private final IAnimationData mAnimationData = new AnimationData();
    private IAnimationListener mAnimationListener;

    // ===========================================================
    // Constructors
    // ===========================================================

    public AnimatedSprite(final float pX, final float pY, final ITiledTextureRegion pTiledTextureRegion, final VertexBufferObjectManager pVertexBufferObjectManager) {
        super(pX, pY, pTiledTextureRegion, pVertexBufferObjectManager, DrawType.DYNAMIC);
    }

    public AnimatedSprite(final float pX, final float pY, final ITiledTextureRegion pTiledTextureRegion, final VertexBufferObjectManager pVertexBufferObjectManager, final ShaderProgram pShaderProgram) {
        super(pX, pY, pTiledTextureRegion, pVertexBufferObjectManager, DrawType.DYNAMIC, pShaderProgram);
    }

    public AnimatedSprite(final float pX, final float pY, final ITiledTextureRegion pTiledTextureRegion, final VertexBufferObjectManager pVertexBufferObjectManager, final DrawType pDrawType) {
        super(pX, pY, pTiledTextureRegion, pVertexBufferObjectManager, pDrawType);
    }

    public AnimatedSprite(final float pX, final float pY, final ITiledTextureRegion pTiledTextureRegion, final VertexBufferObjectManager pVertexBufferObjectManager, final DrawType pDrawType, final ShaderProgram pShaderProgram) {
        super(pX, pY, pTiledTextureRegion, pVertexBufferObjectManager, pDrawType, pShaderProgram);
    }

    public AnimatedSprite(final float pX, final float pY, final ITiledTextureRegion pTiledTextureRegion, final ITiledSpriteVertexBufferObject pTiledSpriteVertexBufferObject) {
        super(pX, pY, pTiledTextureRegion, pTiledSpriteVertexBufferObject);
    }

    public AnimatedSprite(final float pX, final float pY, final ITiledTextureRegion pTiledTextureRegion, final ITiledSpriteVertexBufferObject pTiledSpriteVertexBufferObject, final ShaderProgram pShaderProgram) {
        super(pX, pY, pTiledTextureRegion, pTiledSpriteVertexBufferObject, pShaderProgram);
    }

    public AnimatedSprite(final float pX, final float pY, final float pWidth, final float pHeight, final ITiledTextureRegion pTiledTextureRegion, final VertexBufferObjectManager pVertexBufferObjectManager) {
        super(pX, pY, pWidth, pHeight, pTiledTextureRegion, pVertexBufferObjectManager, DrawType.DYNAMIC);
    }

    public AnimatedSprite(final float pX, final float pY, final float pWidth, final float pHeight, final ITiledTextureRegion pTiledTextureRegion, final VertexBufferObjectManager pVertexBufferObjectManager, final ShaderProgram pShaderProgram) {
        super(pX, pY, pWidth, pHeight, pTiledTextureRegion, pVertexBufferObjectManager, DrawType.DYNAMIC, pShaderProgram);
    }

    public AnimatedSprite(final float pX, final float pY, final float pWidth, final float pHeight, final ITiledTextureRegion pTiledTextureRegion, final VertexBufferObjectManager pVertexBufferObjectManager, final DrawType pDrawType) {
        super(pX, pY, pWidth, pHeight, pTiledTextureRegion, pVertexBufferObjectManager, pDrawType);
    }

    public AnimatedSprite(final float pX, final float pY, final float pWidth, final float pHeight, final ITiledTextureRegion pTiledTextureRegion, final VertexBufferObjectManager pVertexBufferObjectManager, final DrawType pDrawType, final ShaderProgram pShaderProgram) {
        super(pX, pY, pWidth, pHeight, pTiledTextureRegion, pVertexBufferObjectManager, pDrawType, pShaderProgram);
    }

    public AnimatedSprite(final float pX, final float pY, final float pWidth, final float pHeight, final ITiledTextureRegion pTiledTextureRegion, final ITiledSpriteVertexBufferObject pTiledSpriteVertexBufferObject) {
        super(pX, pY, pWidth, pHeight, pTiledTextureRegion, pTiledSpriteVertexBufferObject, PositionColorTextureCoordinatesShaderProgram.getInstance());
    }

    public AnimatedSprite(final float pX, final float pY, final float pWidth, final float pHeight, final ITiledTextureRegion pTiledTextureRegion, final ITiledSpriteVertexBufferObject pTiledSpriteVertexBufferObject, final ShaderProgram pShaderProgram) {
        super(pX, pY, pWidth, pHeight, pTiledTextureRegion, pTiledSpriteVertexBufferObject, pShaderProgram);
    }

    // ===========================================================
    // Getter & Setter
    // ===========================================================

    public boolean isAnimationRunning() {
        return this.mAnimationRunning;
    }

    // ===========================================================
    // Methods for/from SuperClass/Interfaces
    // ===========================================================

    @Override
    protected void onManagedUpdate(final float pSecondsElapsed) {
        super.onManagedUpdate(pSecondsElapsed);

        if (this.mAnimationRunning) {
            final int loopCount = this.mAnimationData.getLoopCount();
            final int[] frames = this.mAnimationData.getFrames();
            final long animationDuration = this.mAnimationData.getAnimationDuration();

            if (!this.mAnimationStartedFired && (this.mAnimationProgress == 0)) {
                this.mAnimationStartedFired = true;
                if (frames == null) {
                    this.setCurrentTileIndex(this.mAnimationData.getFirstFrameIndex());
                } else {
                    this.setCurrentTileIndex(frames[0]);
                }
                this.mCurrentFrameIndex = 0;
                if (this.mAnimationListener != null) {
                    this.mAnimationListener.onAnimationStarted(this, loopCount);
                    this.mAnimationListener.onAnimationFrameChanged(this, AnimatedSprite.FRAMEINDEX_INVALID, 0);
                }
            }
            final long nanoSecondsElapsed = (long) (pSecondsElapsed * TimeConstants.NANOSECONDS_PER_SECOND);
            this.mAnimationProgress += nanoSecondsElapsed;

            if (loopCount == IAnimationData.LOOP_CONTINUOUS) {
                while (this.mAnimationProgress > animationDuration) {
                    this.mAnimationProgress -= animationDuration;
                    if (this.mAnimationListener != null) {
                        this.mAnimationListener.onAnimationLoopFinished(this, this.mRemainingLoopCount, loopCount);
                    }
                }
            } else {
                while (this.mAnimationProgress > animationDuration) {
                    this.mAnimationProgress -= animationDuration;
                    this.mRemainingLoopCount--;
                    if (this.mRemainingLoopCount < 0) {
                        break;
                    } else if (this.mAnimationListener != null) {
                        this.mAnimationListener.onAnimationLoopFinished(this, this.mRemainingLoopCount, loopCount);
                    }
                }
            }

            if ((loopCount == IAnimationData.LOOP_CONTINUOUS) || (this.mRemainingLoopCount >= 0)) {
                final int newFrameIndex = this.mAnimationData.calculateCurrentFrameIndex(this.mAnimationProgress);

                if (this.mCurrentFrameIndex != newFrameIndex) {
                    if (frames == null) {
                        this.setCurrentTileIndex(this.mAnimationData.getFirstFrameIndex() + newFrameIndex);
                    } else {
                        this.setCurrentTileIndex(frames[newFrameIndex]);
                    }
                    if (this.mAnimationListener != null) {
                        this.mAnimationListener.onAnimationFrameChanged(this, this.mCurrentFrameIndex, newFrameIndex);
                    }
                }
                this.mCurrentFrameIndex = newFrameIndex;
            } else {
                this.mAnimationRunning = false;
                if (this.mAnimationListener != null) {
                    this.mAnimationListener.onAnimationFinished(this);
                }
            }
        }
    }

    // ===========================================================
    // Methods
    // ===========================================================

    public void stopAnimation() {
        this.mAnimationRunning = false;
    }

    public void stopAnimation(final int pTileIndex) {
        this.mAnimationRunning = false;
        this.setCurrentTileIndex(pTileIndex);
    }

    public void animate(final long pFrameDurationEach) {
        this.animate(pFrameDurationEach, null);
    }

    public void animate(final long pFrameDurationEach, final IAnimationListener pAnimationListener) {
        this.mAnimationData.set(pFrameDurationEach, this.getTileCount());

        this.initAnimation(pAnimationListener);
    }

    public void animate(final long pFrameDurationEach, final boolean pLoop) {
        this.animate(pFrameDurationEach, pLoop, null);
    }

    public void animate(final long pFrameDurationEach, final boolean pLoop, final IAnimationListener pAnimationListener) {
        this.mAnimationData.set(pFrameDurationEach, this.getTileCount(), pLoop);

        this.initAnimation(pAnimationListener);
    }

    public void animate(final long pFrameDurationEach, final int pLoopCount) {
        this.animate(pFrameDurationEach, pLoopCount, null);
    }

    public void animate(final long pFrameDurationEach, final int pLoopCount, final IAnimationListener pAnimationListener) {
        this.mAnimationData.set(pFrameDurationEach, this.getTileCount(), pLoopCount);

        this.initAnimation(pAnimationListener);
    }

    public void animate(final long[] pFrameDurations) {
        this.animate(pFrameDurations, (IAnimationListener) null);
    }

    public void animate(final long[] pFrameDurations, final IAnimationListener pAnimationListener) {
        this.mAnimationData.set(pFrameDurations);

        this.initAnimation(pAnimationListener);
    }

    public void animate(final long[] pFrameDurations, final boolean pLoop) {
        this.animate(pFrameDurations, pLoop, null);
    }

    public void animate(final long[] pFrameDurations, final boolean pLoop, final IAnimationListener pAnimationListener) {
        this.mAnimationData.set(pFrameDurations, pLoop);

        this.initAnimation(pAnimationListener);
    }

    public void animate(final long[] pFrameDurations, final int pLoopCount) {
        this.animate(pFrameDurations, pLoopCount, null);
    }

    public void animate(final long[] pFrameDurations, final int pLoopCount, final IAnimationListener pAnimationListener) {
        this.mAnimationData.set(pFrameDurations, pLoopCount);

        this.initAnimation(pAnimationListener);
    }

    public void animate(final long[] pFrameDurations, final int pFirstTileIndex, final int pLastTileIndex, final boolean pLoop) {
        this.animate(pFrameDurations, pFirstTileIndex, pLastTileIndex, pLoop, null);
    }

    public void animate(final long[] pFrameDurations, final int pFirstTileIndex, final int pLastTileIndex, final boolean pLoop, final IAnimationListener pAnimationListener) {
        this.mAnimationData.set(pFrameDurations, pFirstTileIndex, pLastTileIndex, pLoop);

        this.initAnimation(pAnimationListener);
    }

    public void animate(final long[] pFrameDurations, final int pFirstTileIndex, final int pLastTileIndex, final int pLoopCount) {
        this.animate(pFrameDurations, pFirstTileIndex, pLastTileIndex, pLoopCount, null);
    }

    public void animate(final long[] pFrameDurations, final int pFirstTileIndex, final int pLastTileIndex, final int pLoopCount, final IAnimationListener pAnimationListener) {
        this.mAnimationData.set(pFrameDurations, pFirstTileIndex, pLastTileIndex, pLoopCount);

        this.initAnimation(pAnimationListener);
    }

    /**
     * Animate specifics frames.
     *
     * @param pFrameDurations must have the same length as pFrames.
     * @param pFrames         indices of the frames to animate.
     */
    public void animate(final long[] pFrameDurations, final int[] pFrames) {
        this.animate(pFrameDurations, pFrames, null);
    }

    /**
     * Animate specifics frames.
     *
     * @param pFrameDurations    must have the same length as pFrames.
     * @param pFrames            indices of the frames to animate.
     * @param pAnimationListener
     */
    public void animate(final long[] pFrameDurations, final int[] pFrames, final IAnimationListener pAnimationListener) {
        this.mAnimationData.set(pFrameDurations, pFrames);

        this.initAnimation(pAnimationListener);
    }

    /**
     * Animate specifics frames.
     *
     * @param pFrameDurations must have the same length as pFrames.
     * @param pFrames         indices of the frames to animate.
     * @param pLoop
     */
    public void animate(final long[] pFrameDurations, final int[] pFrames, final boolean pLoop) {
        this.animate(pFrameDurations, pFrames, pLoop, null);
    }

    /**
     * Animate specifics frames.
     *
     * @param pFrameDurations    must have the same length as pFrames.
     * @param pFrames            indices of the frames to animate.
     * @param pLoop
     * @param pAnimationListener
     */
    public void animate(final long[] pFrameDurations, final int[] pFrames, final boolean pLoop, final IAnimationListener pAnimationListener) {
        this.mAnimationData.set(pFrameDurations, pFrames, pLoop);

        this.initAnimation(pAnimationListener);
    }

    /**
     * Animate specifics frames.
     *
     * @param pFrameDurations must have the same length as pFrames.
     * @param pFrames         indices of the frames to animate.
     * @param pLoopCount
     */
    public void animate(final long[] pFrameDurations, final int[] pFrames, final int pLoopCount) {
        this.animate(pFrameDurations, pFrames, pLoopCount, null);
    }

    /**
     * Animate specifics frames.
     *
     * @param pFrameDurations    must have the same length as pFrames.
     * @param pFrames            indices of the frames to animate.
     * @param pLoopCount
     * @param pAnimationListener
     */
    public void animate(final long[] pFrameDurations, final int[] pFrames, final int pLoopCount, final IAnimationListener pAnimationListener) {
        this.mAnimationData.set(pFrameDurations, pFrames, pLoopCount);

        this.initAnimation(pAnimationListener);
    }

    public void animate(final IAnimationData pAnimationData) {
        this.animate(pAnimationData, null);
    }

    public void animate(final IAnimationData pAnimationData, final IAnimationListener pAnimationListener) {
        this.mAnimationData.set(pAnimationData);

        this.initAnimation(pAnimationListener);
    }

    private void initAnimation(final IAnimationListener pAnimationListener) {
        this.mAnimationStartedFired = false;
        this.mAnimationListener = pAnimationListener;
        this.mRemainingLoopCount = this.mAnimationData.getLoopCount();

        this.mAnimationProgress = 0;
        this.mAnimationRunning = true;
    }

    // ===========================================================
    // Inner and Anonymous Classes
    // ===========================================================

    public static interface IAnimationListener {
        // ===========================================================
        // Constants
        // ===========================================================

        // ===========================================================
        // Fields
        // ===========================================================

        /**
         * @param pAnimatedSprite
         * @param pInitialLoopCount is {@link AnimatedSprite#LOOP_CONTINUOUS} when {@link AnimatedSprite} loops infinitely.
         */
        public void onAnimationStarted(final AnimatedSprite pAnimatedSprite, final int pInitialLoopCount);

        /**
         * @param pAnimatedSprite
         * @param pOldFrameIndex  equals {@link AnimatedSprite#FRAMEINDEX_INVALID}, the first time {@link IAnimationListener#onAnimationFrameChanged(AnimatedSprite, int, int)} is called.
         * @param pNewFrameIndex  the new frame index of the currently active animation.
         */
        public void onAnimationFrameChanged(final AnimatedSprite pAnimatedSprite, final int pOldFrameIndex, final int pNewFrameIndex);

        /**
         * @param pAnimatedSprite
         * @param pRemainingLoopCount is {@link AnimatedSprite#LOOP_CONTINUOUS} when {@link AnimatedSprite} loops infinitely.
         * @param pInitialLoopCount   is {@link AnimatedSprite#LOOP_CONTINUOUS} when {@link AnimatedSprite} loops infinitely.
         */
        public void onAnimationLoopFinished(final AnimatedSprite pAnimatedSprite, final int pRemainingLoopCount, final int pInitialLoopCount);

        public void onAnimationFinished(final AnimatedSprite pAnimatedSprite);
    }
}
